package stencil.core.engine.scene 
{
	import flash.geom.Point;
	import flash.geom.Rectangle;
	import flash.utils.Dictionary;
	import stencil.core.engine.Bounds;
	import stencil.core.engine.IGrid;
	import stencil.core.engine.IMapArea;
	import stencil.core.engine.IMapGraph;
	import stencil.core.engine.ISceneObject;
	import stencil.core.engine.Position;
	import stencil.core.engine.TileSettings;
	import stencil.core.errors.ArgumentIsNullError;
	import stencil.core.errors.NotImplementedMethodError;
	import stencil.core.errors.RequiredPropertyError;
	
	/**
	 * ...
	 * @author Max Zhuravkov
	 */
	public class AbstractSceneStructure implements ISceneStructure{
		
		private var _tileGrid:IGrid;
		
		private var _objectGrid:IGrid;
		
		private var _boundaries:Rectangle;
		
		private var _globalBoundaries:Rectangle;
		
		private var _sceneNodesAndOtherObjects:Dictionary = new Dictionary();
		
		private var _objects:Dictionary = new Dictionary();
		
		private var _sceneNodes:Array = [];
		
		private var _tileSettings:TileSettings;
		
		private var _mapGraph:IMapGraph;
		
		private var _lists:Dictionary = new Dictionary();
		
		private var _objectRegistry:ISceneObjectRegistry;
		
		//------------------------------------------------------------------------------------------------
		// Constructor
		//------------------------------------------------------------------------------------------------
		
		public function AbstractSceneStructure(boundaries:Rectangle) {
			this._boundaries = boundaries;
			this._objectRegistry = this.createObjectRegistry(this._objects);
		}
		
		//------------------------------------------------------------------------------------------------
		// Public methods
		//------------------------------------------------------------------------------------------------
		
		/**
		 * @inheritDoc
		 */
		public function set objectGrid(value:IGrid):void {
			if (this._objectGrid == value) {
				return;
			}
			this._objectGrid = value;
		}
		
		public function get objectGrid():IGrid {
			return this._objectGrid;
		}
		
		/**
		 * @inheritDoc
		 */
		public function set tileGrid(value:IGrid):void {
			if (this._tileGrid == value) {
				return;
			}
			this._tileGrid = value;
		}
		
		public function get tileGrid():IGrid {
			return this._tileGrid;
		}
		
		/**
		 * @inheritDoc
		 */
		public function get boundaries():Rectangle {
			return this._boundaries;
		}
		
		/**
		 * @inheritDoc
		 */
		public function get globalBoundaries():Rectangle {
			return this._globalBoundaries;
		}
		
		/**
		 * @inheritDoc
		 */
		public function set tileSettings(value:TileSettings):void {
			if (this._tileSettings == value) {
				return;
			}
			this._tileSettings = value;
		}
		
		public function get tileSettings():TileSettings {
			return this._tileSettings;
		}
		
		/**
		 * @inheritDoc
		 */
		public function set mapGraph(value:IMapGraph):void {
			if (this._mapGraph == value) {
				return;
			}
			this._mapGraph = value;
		}
		
		public function get mapGraph():IMapGraph {
			return this._mapGraph;
		}
		
		/**
		 * @inheritDoc
		 */
		public function get sceneNodes():Array {
			return this._sceneNodes;
		}
		
		/**
		 * @private
		 */
		public function get lists():Array {
			return this._objectRegistry.lists;
		}
		
		//------------------------------------------------------------------------------------------------
		// Public methods
		//------------------------------------------------------------------------------------------------
		
		
		public function init():void {
			if (!this.objectGrid) {
				throw new RequiredPropertyError(this, "objectGrid");
			}
			
			if (!this.tileGrid) {
				throw new RequiredPropertyError(this, "tileGrid");
			}
			
			if (!this.tileSettings) {
				throw new RequiredPropertyError(this, "tileSettings");
			}
			
			this._globalBoundaries = this.calculateGlobalBounds();
		}
		
		
		/**
		 * @inheritDoc
		 */
		public function createList(name:String, filter:Function = null):Array {
			return this._objectRegistry.createList(name, filter);
		}
		
		/**
		 * @inheritDoc
		 */
		public function getList(name:String):Array {
			return this._objectRegistry.getList(name);
		}
		
		/**
		 * @inheritDoc
		 */
		public function hasList(name:String):Boolean {
			return this._objectRegistry.hasList(name);
		}
		
		/**
		 * @inheritDoc
		 */
		public function removeList(name:String):void {
			this._objectRegistry.removeList(name);
		}
		
		/**
		 * @inheritDoc
		 */
		public function removeLists():void {
			this._objectRegistry.removeLists();
		}
		
		/**
		 * @inheritDoc
		 */
		public function getSceneNode(p:Position):ISceneNode {
			var sw:int = this.tileSettings.sectionWidth;
			var sh:int = this.tileSettings.sectionHeight;
			var nx:int = sw * Math.floor(p.x / sw);
			var ny:int = sh * Math.floor(p.y / sh);
			var nz:int = p.z;
			
			
			for each(var node:ISceneNode in this.sceneNodes) {
				var position:Position = node.position;
				if (position.x == nx && position.y == ny && position.z == nz) {
					return node;
				}
			}
			
			return null;
		}
		
		
		/**
		 * @inheritDoc
		 */
		public final function addObject(object:ISceneObject):Boolean {
			this.checkObject(object);
			
			if (this.addObjectToStage(object)) {
				this.objectAdded(object);
				return true;
			}else {
				this.objectRemoved(object);
				return false;
			}
		}
		
		/**
		 * @inheritDoc
		 */
		public final function removeObject(object:ISceneObject):Boolean {
			this.checkObject(object);
			
			if (this.removeObjectFromStage(object)) {
				this.objectRemoved(object);
				return true;
			}else{
				return false;
			}
		}
		
		/**
		 * @inheritDoc
		 */
		public final function updateObject(object:ISceneObject):Boolean {
			this.checkObject(object);
			
			if (this.updateObjectAtStage(object)) {
				this.objectUpdated(object);
				return true;
			}else {
				this.objectRemoved(object);
				return false;
			}
		}
		
		/**
		 * @inheritDoc
		 */
		public function containsObject(object:ISceneObject):Boolean {
			this.checkObject(object);
			
			return this._sceneNodesAndOtherObjects[object.id] ? true : false;
		}
		
		/**
		 * @inheritDoc
		 */
		public function getObject(id:String):ISceneObject {
			if (id == null) {
				throw new ArgumentIsNullError("id");
			}
			return this._sceneNodesAndOtherObjects[id];
		}
		
		/**
		 * @inheritDoc
		 */
		public function getObjects(rect:Rectangle, filter:Function = null):Array {
			
			return null;
		}
		
		/**
		 * @inheritDoc
		 */
		public function getObjectsAt(p:Position, filter:Function = null):Array {
			throw new NotImplementedMethodError("getObjectsAt")
			return null;
		}
		
		/**
		 * @inheritDoc
		 */
		public function getArea(rect:Rectangle, filter:Function = null):IMapArea {
			throw new NotImplementedMethodError("getArea");
			return null;
		}
		
		/**
		 * Removes all object from scene but does not dispose them.
		 */
		public function dispose():void {
			this._objectRegistry.removeLists();
		}
		
		//------------------------------------------------------------------------------------------------
		// Protected methods
		//------------------------------------------------------------------------------------------------
		
		protected function calculateGlobalBounds():Rectangle {
			return this.tileGrid.getRect(this.boundaries);
		}
		
		/**
		 * Adds the given scene object to this scene structure.
		 * 
		 * @param	o	the scene to be added.
		 */
		protected function addObjectToStage(o:ISceneObject):Boolean {
			return false;
		}
		
		/**
		 * Updates object position within this scene structure.
		 * 
		 * @param	o	the scene object.
		 */
		protected function updateObjectAtStage(o:ISceneObject):Boolean {
			return false;
		}
		
		/**
		 * Removes the specifed scene object from this scene structure.
		 * 
		 * @param	o	the scene object to removed.
		 */
		protected function removeObjectFromStage(o:ISceneObject):Boolean {
			return false;
		}
		
		/**
		 * Creates scene object registry.
		 * 
		 * @param	objects	the object map.
		 * 
		 * @return
		 */
		protected function createObjectRegistry(objects:Dictionary):ISceneObjectRegistry {
			return new SceneObjectRegistry(objects);
		}
		
		/**
		 * Validates the argument of 'addObject', 'updateObject' and 'removeObject' method.
		 * 
		 * @param	object	the scene object.
		 */
		protected function checkObject(object:ISceneObject):void {
			if (!object) {
				throw new ArgumentIsNullError("object");
			}
			if (!object.id) {
				throw new Error("Object id should be not null. Type alias=" + object.type + ".")
			}
			
			var o:ISceneObject = this._sceneNodesAndOtherObjects[object.id];
			if (o && o != object) {
				throw new ArgumentError("Object with identifier " + o.id + " is already exists. " + o);
			}
		}
		
		/**
		 * Called when the given object was added to scene.
		 * 
		 * @param	o	the scene object.
		 */
		protected function objectAdded(o:ISceneObject):void {
			if (o is ISceneNode) {
				var index:int = this._sceneNodes.indexOf(o);
				
				if (index == -1) {
					this._sceneNodes.push(o);
				}
			}else {
				
				if (o.flagStatic) {
					var nodes:Array = this.findNodes(o.bounds.rect);
					
					for each(var node:ISceneNode in nodes) {
						node.put(o);
					}
				}
				
				if (this._objectRegistry.lists.length > 0) {
					for each(var list:ISceneObjectList in this._objectRegistry.lists) {
						list.add(o);
					}
				}
				
				this._objects[o.id] = o;
			}
			
			this._sceneNodesAndOtherObjects[o.id] = o;
		}
		
		/**
		 * Called when the given object was updated.
		 * 
		 * @param	o	the scene object.
		 */
		protected function objectUpdated(o:ISceneObject):void {
			this.objectAdded(o);
		}
		
		/**
		 * Called when the given object was removed from scene.
		 * 
		 * @param	o
		 */
		protected function objectRemoved(o:ISceneObject):void {
			if (o is ISceneNode) {
				var index:int = this._sceneNodes.indexOf(o);
				
				if (index != -1) {
					this._sceneNodes.splice(index, 1);
				}
			}else {
				if (o.flagStatic) {
					var nodes:Array = this.findNodes(o.bounds.rect);
					
					for each(var node:ISceneNode in nodes) {
						node.remove(o);
					}
				}
				
				if (this._objectRegistry.lists.length > 0) {
					for each(var list:ISceneObjectList in this._objectRegistry.lists) {
						list.remove(o);
					}
				}
				
				delete this._objects[o.id];
			}
			
			delete this._sceneNodesAndOtherObjects[o.id];
		}
		
		/**
		 * Returns an array of nodes intercepting the given area.
		 * 
		 * @param	rect	the area.
		 * 
		 * @return	an array of scene nodes.
		 */
		protected function findNodes(rect:Rectangle):Array {
			throw new NotImplementedMethodError("findNodes")
			return null;
		}
		
		protected function updateObjectProperties(object:ISceneObject):void {
			
			if (!(object is ISceneNode)) {
				var position:Position = object.position;
				var p:Point = this.objectGrid.getCellPosition(position.x, position.y, false);
				var bounds:Bounds = object.bounds;
				if (!bounds) {
					throw new Error("Bounds has not been set! Object#" + object.id + " type alias=" + object.type + ".")
				}
				bounds.translate(p.x, p.y);
			}
		}
	
	}

}